namespace Prawks.SqlMethods.CodeGeneration.Web
{
    #region Imports

    using System;
    using System.Reflection;
    using System.Xml;
    using System.Data.SqlClient;
    using System.CodeDom;
    using System.Web.Compilation;
    using System.IO;
    using System.Xml.Schema;
    using System.Diagnostics;

    #endregion    

    public sealed class SqlMethodsBuildProvider : BuildProvider
    {
        private readonly bool _isDebuggable;

        public SqlMethodsBuildProvider()
        {
            _isDebuggable = GetType().Assembly.IsDefined(typeof(DebuggableAttribute), false);
        }

        public override void GenerateCode(AssemblyBuilder assemblyBuilder)
        {
            DateTime startTime = DateTime.Now;

            using (TextWriter output = TextWriter.Null)
            {
                try
                {
                    //
                    // Load the input XML file with validation.
                    //

                    XmlDocument document = new XmlDocument();

                    using (Stream inputStream = OpenStream())
                    {
                        Exception validationError = null;

                        XmlReaderSettings xmlSettings = new XmlReaderSettings();

                        xmlSettings.ValidationType = ValidationType.Schema;
                        xmlSettings.Schemas.Add(GetXmlSchema());
                        xmlSettings.ValidationEventHandler += delegate(object sender, ValidationEventArgs args) { validationError = args.Exception; };

                        XmlReader reader = XmlReader.Create(inputStream, xmlSettings);

                        try
                        {
                            document.Load(reader);
                        }
                        catch (XmlException e)
                        {
                            throw new CodeGeneratorException(e.Message, e);
                        }

                        if (validationError != null)
                            throw new CodeGeneratorException(validationError.GetBaseException().Message, validationError);
                    }

                    //
                    // Get the configuration infoset.
                    //

                    XmlElement typeElement = document.DocumentElement;

                    TypeName typeName = new TypeName();
                    typeName.Name = typeElement.GetAttribute("name");
                    typeName.Namespace = typeElement.GetAttribute("namespace");
                    string baseTypeName = typeElement.GetAttribute("inherits");
                    string factoryName = typeElement.GetAttribute("factoryName");
                    bool hasFactory = !XmlConvert.ToBoolean(typeElement.GetAttribute("noFactory"));
                    bool isPartial = XmlConvert.ToBoolean(typeElement.GetAttribute("isPartial"));

                    if (typeName.Name.Length == 0)
                        throw new CodeGeneratorException("Missing name for the type to generate.");

                    //
                    // Create namespace and import other namespaces into it.
                    // If no namespace was specified, then assume the global one.
                    //

                    CodeNamespace codeNamespace = new CodeNamespace(typeName.Namespace);
                    GenerateVersionComment(codeNamespace);
                    codeNamespace.Imports.Add(new CodeNamespaceImport("Prawks.SqlMethods"));

                    //
                    // Define a new abstract class and it to the namespace.
                    //

                    CodeTypeDeclaration codeType = new CodeTypeDeclaration(typeName.Name);
                    codeType.IsClass = true;
                    codeType.IsPartial = isPartial;
                    codeType.TypeAttributes |= System.Reflection.TypeAttributes.Abstract;

                    if (baseTypeName.Length != 0)
                        codeType.BaseTypes.Add(baseTypeName);

                    codeNamespace.Types.Add(codeType);

                    //
                    // Add a default public constructor because the default one
                    // generated by the compiler may be marked as protected 
                    // (family member access) since the containing class is abstract.
                    // The C# compiler, for example, does this.
                    //

                    CodeConstructor codeConstructor = new CodeConstructor();
                    codeConstructor.Attributes = MemberAttributes.Public;
                    codeType.Members.Add(codeConstructor);

                    //
                    // Generate da methods! (only SqlMethods supported at present)
                    //

                    int procedureCount = 0;

                    foreach (XmlElement methodsElement in typeElement.SelectNodes("sqlMethods"))
                        procedureCount += GenerateSqlMethods(methodsElement, codeType, output);

                    //
                    // Create a factory method if specified.
                    //

                    if (hasFactory)
                    {
                        codeNamespace.Imports.Add(new CodeNamespaceImport("Prawks.CodeProvisioning"));

                        CodeMemberMethod codeFactoryMethod = new CodeMemberMethod();

                        codeFactoryMethod.Name = factoryName;
                        codeFactoryMethod.Attributes = MemberAttributes.Public | MemberAttributes.Static;
                        codeFactoryMethod.ReturnType = new CodeTypeReference(codeType.Name);
                        codeFactoryMethod.Statements.Add(new CodeMethodReturnStatement(new CodeCastExpression(codeType.Name, new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("DynamicTypeImpl"), "CreateInstance", new CodeExpression[] { new CodeTypeOfExpression(codeType.Name) }))));

                        codeType.Members.Add(codeFactoryMethod);
                    }

                    //
                    // Put the namespace into a compile unit and generate code from it.
                    //

                    CodeCompileUnit codeUnit = new CodeCompileUnit();
                    codeUnit.Namespaces.Add(codeNamespace);

                    if (_isDebuggable)
                        assemblyBuilder.CodeDomProvider.GenerateCodeFromCompileUnit(codeUnit, output, new System.CodeDom.Compiler.CodeGeneratorOptions());

                    assemblyBuilder.AddCodeCompileUnit(this, codeUnit);

                    string fullTypeName = codeNamespace.Name.Length == 0 ?
                        codeType.Name : codeNamespace.Name + "." + codeType.Name;

                    if (procedureCount > 0)
                        output.WriteLine();

                    output.WriteLine(
                        string.Format("Generated {0} method(s) in {1} in {2} second(s)",
                        codeType.Members.Count.ToString("n0"), fullTypeName,
                        (DateTime.Now - startTime).TotalSeconds.ToString("n4")));
                }
                catch (Exception e)
                {
                    output.WriteLine();
                    output.WriteLine("Generation of SQL methods failed for the following reason:");
                    output.WriteLine(e.GetBaseException().Message);

                    if (_isDebuggable)
                        output.WriteLine(e);

                    throw;
                }
            }
        }

        private static int GenerateSqlMethods(XmlElement element, CodeTypeDeclaration codeType, TextWriter output)
        {
            Debug.Assert(element != null);
            Debug.Assert(codeType != null);
            Debug.Assert(output != null);

            string connectionString = element.GetAttribute("connectionString");
            bool useSqlTypes = XmlConvert.ToBoolean(element.GetAttribute("useSqlTypes"));

            if (connectionString.Length == 0)
                throw new CodeGeneratorException("Connection is empty.");

            output.WriteLine("Connecting to database ...");

            SqlConnection connection = new SqlConnection(connectionString);
            connection.Open();

            using (connection)
            {
                output.WriteLine(string.Format("Connected to '{0}' on '{1}'", connection.Database, connection.DataSource));

                SqlMethodsGenerator sqlMethodsGenerator = new SqlMethodsGenerator(codeType);
                sqlMethodsGenerator.UseNullableTypes = useSqlTypes;

                int procedureCount = 0;
                sqlMethodsGenerator.GeneratingProcedure += delegate(object sender, FeedbackEventArgs e)
                {
                    if (procedureCount == 0)
                        output.WriteLine();

                    output.WriteLine("{0,5} > {1}", (++procedureCount).ToString(), e.Text);
                };
                
                //
                // Create the methods of the type from SQL stored procedures.
                //

                XmlNodeList procedures = element.SelectNodes("procedures/procedure");

                string[] procedureNames = new string[procedures.Count];

                for (int i = 0; i < procedures.Count; i++)
                    procedureNames[i] = ((XmlElement)procedures[i]).GetAttribute("name");

                if (procedureNames.Length == 0)
                    sqlMethodsGenerator.AddUserProcedures(connection);
                else
                    sqlMethodsGenerator.AddProcedures(connection, procedureNames);

                return procedureCount;
            }
        }

        private void GenerateVersionComment(CodeNamespace codeNamespace)
        {
            codeNamespace.Comments.Add(new CodeCommentStatement(string.Empty));
            codeNamespace.Comments.Add(new CodeCommentStatement("This source code was auto-generated by:"));
            codeNamespace.Comments.Add(new CodeCommentStatement(GetType().FullName));
            codeNamespace.Comments.Add(new CodeCommentStatement(string.Empty));
        }

        [Serializable]
        private sealed class TypeName
        {
            private string _namespace;
            private string _name;

            public string Name
            {
                get { return _name ?? string.Empty; }
                set { _name = value; }
            }

            public string Namespace
            {
                get { return _namespace ?? string.Empty; }
                set { _namespace = value; }
            }
        }

        private static XmlSchema GetXmlSchema()
        {
            Assembly assembly = typeof(SqlMethodsGenerator).Assembly;
            using (Stream stream = assembly.GetManifestResourceStream(typeof(SqlMethodsGenerator), "DbMethods.xsd"))
                return XmlSchema.Read(stream, null);
        }
    }
}
